using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
using Aaa.GeneratedPb;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Diagnostics;
using System.Text.RegularExpressions;

#region
static class S{
    public static readonly string NOR      = Console.IsOutputRedirected ? "" : "\x1b[39m",
    RED = Console.IsOutputRedirected ? "" : "\x1b[91m",
    GREEN = Console.IsOutputRedirected ? "" : "\x1b[92m",
    YELLOW = Console.IsOutputRedirected ? "" : "\x1b[93m",
    BLUE = Console.IsOutputRedirected ? "" : "\x1b[94m",
    MAGENTA = Console.IsOutputRedirected ? "" : "\x1b[95m",
    CYAN = Console.IsOutputRedirected ? "" : "\x1b[96m",
    GREY = Console.IsOutputRedirected ? "" : "\x1b[97m",
    BOLD = Console.IsOutputRedirected ? "" : "\x1b[1m",
    NOBOLD = Console.IsOutputRedirected ? "" : "\x1b[22m",
    UNDERLINE = Console.IsOutputRedirected ? "" : "\x1b[4m",
    NOUNDERLINE = Console.IsOutputRedirected ? "" : "\x1b[24m",
    REVERSE = Console.IsOutputRedirected ? "" : "\x1b[7m",
    NOREVERSE = Console.IsOutputRedirected ? "" : "\x1b[27m";

    public static ILogger L = LoggerFactory.Create(builder =>{
        builder
            .AddFilter("🦜", LogLevel.Debug) // 🦜: filter
            .AddSimpleConsole(options => {
            options.SingleLine = true;
            options.TimestampFormat = "HH:mm:ss ";
        });
            }).CreateLogger("🦜");
    public static void LogInfo(string msg) => L.LogInformation(msg);
    public static void LogDebug(string msg) => L.LogDebug(msg);
}

static class N {
    public static JsonException E0(string s) => new JsonException(s);
    public static JsonException E1(string p, JsonElement o) => new JsonException($"Property `{p}` not found or is null in {o.GetType().Name} object: {o.ToString()}");
    public static string GetString(JsonElement o, string p) => o.GetProperty(p).GetString() ?? throw E1(p, o);
    public static string GetString(JsonObject o, string p) => N.GetString(JsonSerializer.Deserialize<JsonElement>(o), p);

    public const string
        InitialInput = "InitialInput",
        TextSoW = "TextSoW",
        TestCase = "TestCase",
        NetworkSetup = "NetworkSetup",
        TestScript = "TestScript",
        TestResult = "TestResult",

        Maroon = InitialInput, Red = TextSoW, Yellow = TestCase,Orange = NetworkSetup,
        Blue = TestScript,Purple = TestResult,

        Todo = "Todo", Queue ="Queue", Caesar = "Caesar";
    public static string toJson(object o) => JsonSerializer.Serialize(o, new JsonSerializerOptions {
            IncludeFields = true,
            WriteIndented = true,
    });

    public static bool checkUserToken(string u, string creator){
        if (u.Trim() == N.Caesar) return true; // 🦜 : Caesar is the super user
        return u == creator;
    }

    public static W<string> Error(string msg, int code = -1) => new W<string>(null, code, msg);
    public static W<T> OK<T>(T? data) => new W<T>(data, 0, "");
    public static W<string> OK() => new W<string>("OK", 0, "");

    public static void h(IApplicationBuilder x){
        x.Run(async c => {
            c.Response.StatusCode = 200;
            c.Response.ContentType = "application/json";
            Exception e = c.Features.Get<IExceptionHandlerFeature>()?.Error;
            await c.Response.WriteAsJsonAsync(
                new W<string>(null, 500,
                              $" : Below is the catched error: {e?.Message}" +
                              (e?.InnerException != null ? $", Inner: {e.InnerException.Message}" : "")
                )
            );
        });
    }
}

interface I01Backable {
    // deal with the backend
    Task<C01_TextSoW> F0_ParseInitialInput(C00_InitialInput i);
    Task<IList<C03_TestCase>> F1_TextSoW2Testcases(C01_TextSoW i);
    Task<C02_NetworkSetup> F2_GetCommonNetworkSetup(C01_TextSoW i);
    Task<C04_TestScript> F3_GenerateScript(C03_TestCase c, C02_NetworkSetup n);
    Task<C05_TestResult> F4_RunAndGetLog(C04_TestScript s, C02_NetworkSetup n);


    public class Mock: I01Backable {
        public async Task<C01_TextSoW> F0_ParseInitialInput(C00_InitialInput i) {
            // S.LogInfo($"🦜 Mock: F0_ParseInitialInput called with: {S.GREEN}{N.toJson(i)}{S.NOR}");
            Console.WriteLine($" Mock: F0_ParseInitialInput called with: {S.GREEN}{N.toJson(i)}{S.NOR}");
            return (C01_TextSoW) C01_TextSoW.Mock().setGenerated();
        }
        public async Task<IList<C03_TestCase>> F1_TextSoW2Testcases(C01_TextSoW t) {
            S.LogInfo($" Mock: F1_TextSoW2Testcases called with: {S.GREEN}{N.toJson(t)}{S.NOR}");
            //  : return a mocked list of test cases
            IList<C03_TestCase> l = new List<C03_TestCase>();
            for (int i = 0; i < 5; i++) {
                l.Add((C03_TestCase) C03_TestCase.Mock().setGenerated());
            }
            return l;
        }
        public async Task<C02_NetworkSetup> F2_GetCommonNetworkSetup(C01_TextSoW i) {
            S.LogInfo($" Mock: F2_GetCommonNetworkSetup called with:{S.GREEN} {N.toJson(i)}{S.NOR}");
            return (C02_NetworkSetup) C02_NetworkSetup.Mock().setGenerated();
        }
        public async Task<C04_TestScript> F3_GenerateScript(C03_TestCase c, C02_NetworkSetup n) {
            S.LogInfo($" Mock: F3_GenerateScript called with: {S.GREEN}{N.toJson(c)}{S.NOR} and {S.BLUE}{N.toJson(n)}{S.NOR}");
            //  : return a mocked test script
            return (C04_TestScript) C04_TestScript.Mock().setGenerated();
        }
        public async Task<C05_TestResult> F4_RunAndGetLog(C04_TestScript s, C02_NetworkSetup n) {
            if (s.notes.Contains("[MockError]")) {
                // 🦜 : if the script contains [MockError], throw an exception
                throw new InvalidOperationException("Mock error triggered by script.");
            }
            S.LogInfo($" Mock: F4_RunAndGetLog called with: {S.GREEN}{N.toJson(s)}{S.NOR} and {S.BLUE}{N.toJson(n)}{S.NOR}");
            return (C05_TestResult) C05_TestResult.Mock().setGenerated();
        }
    }
}

interface I02Poolable {
    void Add(TChain item);
}

interface I03Queueable {
    void Enq(TChain item);      // enqueue
}

interface I04Archivable {
    Task Arch(TChain item);     // archive
}

interface I05ForRESTPoolAccessible: I06ForRESTChainInfoGettable{
    void F01_updateCh(TChain ch); //ok or throw
    TChain? F03_getChain(string cid, string u);

    void F04_notifyPauseChain(string cid);
    void F05_notifyResumeChain(string cid);
}

interface I06ForRESTChainInfoGettable{
    IList<TChain.Info> F02_getChInfos(string u);
}

interface I08ForRESTChainGettable: I06ForRESTChainInfoGettable{
    Task<TChain?> F03_getChainAsync(string cid, string u);
}

public record PG2<T>(IList<T> l, long q02_totalCount, long totalPages);
public record W<T>(T? data, int code, string message=""); // 🦜 : names matter

interface I07RESTServable {
    W<string> R00_ping() {
        // 🦜 : return the status of the service
        return N.OK("Service is running");
    }
    static async Task<string> getStringBody(HttpRequest r){
        r.EnableBuffering(); // Allow multiple reads of the body
        using var reader = new StreamReader(r.Body, System.Text.Encoding.UTF8, leaveOpen: true);
        return await reader.ReadToEndAsync();
    }
    W<PG2<TChain.Info>> R02_getRChainInfos(bool todo, [FromHeader(Name = "USER-TOKEN-HI")] string u = "", int q01_pageIdx = 1,  int q03_pageSize = 0xff){
        return R02_getRChainInfos0(todo:todo, u:u, q01_pageIdx:q01_pageIdx, q03_pageSize:q03_pageSize);
    }
    async Task<W<string>> R05_getChainById(string cid, [FromHeader(Name = "USER-TOKEN-HI")] string u = ""){
        return await R05_getChainById0(cid:cid, u:u);
    }
    async Task<W<string>> R06_postChain(HttpRequest r){
        return this.R06_postChain0(ch: await I07RESTServable.getStringBody(r));
    }

    W<PG2<TChain.Info>> R02_getRChainInfos0(bool todo,  string u, int q01_pageIdx,  int q03_pageSize);
    W<string> R03_pauseChain(string cid);
    W<string> R04_resumeChain(string cid);
    // 🦜 : Need custom ToJson, so return string
    Task<W<string>> R05_getChainById0(string cid, string u = "");
    W<string> R06_postChain0(string ch);

    public class Mock: I07RESTServable {
        Dictionary<string,TChain> M {get;init;}
        public Mock(string p =  "/home/me/repo/work/n3c/front3/src/c02-posts.tmp.json", // the creative {title: } file
                    string p0 = "/home/me/repo/work/n3c/bk/T.json"){ // the mocked Tchain template

            JsonDocument d = JsonDocument.Parse(File.ReadAllText(p));
            List<JsonElement> l = d.RootElement.EnumerateArray().ToList();
            this.M = new Dictionary<string, TChain>();
            for (int i = 0; i < l.Count; i++) {
                var c = TChain.Mock(cid:$"cid-{i:000}",
                                  projectId:$"project-{i % 5:000}",
                                  projectName:l[i].GetProperty("title").GetString() ?? throw new Exception("projectName is null"),
                                  p0:p0);
                c.L[0].LogInfo("This is a mocked log.");
                c.L[0].LogInfo("This is another mocked log.");
                this.M.Add(c.cid, c); // add to the mock dictionary
            }
        }
        // 🦜 : default ToJson is fine for TChain.Info
        public W<PG2<TChain.Info>> R02_getRChainInfos0(bool todo, string u = "", int q01_pageIdx = 1,  int q03_pageSize = 10) {
            S.LogInfo($"⚙️ R02_getRChainInfos0: todo={S.CYAN}{todo}{S.NOR}, u={S.CYAN}{u}{S.NOR}, page={S.CYAN}{q01_pageIdx}{S.NOR}, q03_pageSize={S.CYAN}{q03_pageSize}{S.NOR}");
            IList<TChain.Info> L = new List<TChain.Info>();
            string[] st = ["Todo", "Queue n/N"];
            foreach (var (i, v) in this.M.Select((x, i) => (i, x.Value))) {
                L.Add(v.GetInfo(status: st[i % st.Length])); // add with status
            }
            return N.OK(RestMgr.getPagedResult(l:L, q01_pageIdx:q01_pageIdx, q03_pageSize:q03_pageSize));
        }
        public W<string> R03_pauseChain(string cid) {
            S.LogInfo($"⚙️ R03_pauseChain: {S.CYAN}{cid}{S.NOR}");
            return N.OK("Mock: Chain paused.");
        }
        public W<string> R04_resumeChain(string cid) {
            S.LogInfo($"⚙️ R04_resumeChain: {S.CYAN}{cid}{S.NOR}");
            return N.OK("Mock: Chain resumed.");
        }
        public async Task<W<string>> R05_getChainById0(string cid, string u = "") {
            // 🦜 : for now let's ignore the user token..
            if (this.M.TryGetValue(cid, out TChain? ch)) {
                return N.OK(N.toJson(ch)); // found the chain
            }
            return N.Error($"Chain with cid {cid} not found.", 404);
        }
        public W<string> R06_postChain0(string ch) {
            S.LogInfo($"⚙️ R06_postChain0: body:{S.GREEN}{ch}{S.NOR}");
            var c0 = TChain.FromJson(ch); // update or add the chain
            this.M[c0.cid] = c0;
            return N.OK("Mock: Chain posted.");
        }
    }
}

class GrpcLiaison: I01Backable{
    Greeter.GreeterClient cl {get;init;}
    GrpcChannel ch {get;init;}
    int ts {get;init;}                // timeout in seconds for gRPC calls
    DateTime ddl  => DateTime.UtcNow.AddSeconds(this.ts);

    public GrpcLiaison(string url="http://192.168.1.181:50052", int timeout_sec = 600 // a long time 🦜 = 10m
    ){
        this.ch = GrpcChannel.ForAddress(url);
        this.cl = new Greeter.GreeterClient(this.ch);
        this.ts = timeout_sec;
    }

    static void addLogMaybe<T, T2>(Grpc.Core.AsyncUnaryCall<T> c, ref T2 r)
        where T2 : ChainItem
    { // 🦜 : Even though r is useless here since we are passing a refence type, it's harmless and make it clear that this function is to "update" r
        string? y = c.GetTrailers().GetValue("log-hi");
        // <2025-08-07 Thu> : 🦜 : Also.. set generated here
        r.setGenerated();
        if (!string.IsNullOrEmpty(y)){
            r.LogInfo(y);
        }
    }
    public virtual async Task<C01_TextSoW> F0_ParseInitialInput(C00_InitialInput i) {
        // 🦜 : send the initial input to the backend and get the processed TextSoW
        // var r = await this.cl.F0_ParseInitialInputAsync(i.toPb());
        // Grpc.Core.AsyncUnaryCall<TResponse>
        var c = this.cl.F0_ParseInitialInputAsync(i.toPb(), deadline: this.ddl); // 🦜 : set the deadline for the call
        var r = C01_TextSoW.FromPb(await c.ResponseAsync);
        GrpcLiaison.addLogMaybe(c, ref r);
        return r;
    }

    public async Task<IList<C03_TestCase>> F1_TextSoW2Testcases(C01_TextSoW i) {
        // 🦜 : send the TextSoW to the backend and get the list of TestCases
        var c = this.cl.F1_TextSoW2TestcasesAsync(i.toPb(),deadline: this.ddl);
        var result = C03_TestCase.FromPb(await c.ResponseAsync);
        // Add log to the last test case if list is not empty
        if (result.Count > 0) {
            var lastItem = result.Last();
            GrpcLiaison.addLogMaybe(c, ref lastItem);
        }
        return result;
    }
    public virtual async Task<C02_NetworkSetup> F2_GetCommonNetworkSetup(C01_TextSoW i) {
        // 🦜 : send the TextSoW to the backend and get the common NetworkSetup
        var c = this.cl.F2_GetCommonNetworkSetupAsync(i.toPb(),deadline: this.ddl);
        var r = C02_NetworkSetup.FromPb(await c.ResponseAsync);
        GrpcLiaison.addLogMaybe(c, ref r);
        return r;
    }
    public async Task<C04_TestScript> F3_GenerateScript(C03_TestCase c, C02_NetworkSetup n) {
        // 🦜 : send the TestCase and NetworkSetup to the backend and get the TestScript
        var call = this.cl.F3_GenerateScriptAsync(new X {TestCase = c.toPb(), NetworkSetup = n.toPb()},deadline: this.ddl);
        var r = C04_TestScript.FromPb(await call.ResponseAsync);
        GrpcLiaison.addLogMaybe(call, ref r);
        return r;
    }
    public virtual async Task<C05_TestResult> F4_RunAndGetLog(C04_TestScript s, C02_NetworkSetup n) {

        // 🦜 : send the TestScript to the backend and get the TestResult
        var c = this.cl.F4_RunAndGetLogAsync(new TestUnit {
                NetworkSetup = n.toPb(), Script = s.pyscript
            },deadline: this.ddl);
        var r = C05_TestResult.FromPb(await c.ResponseAsync);
        GrpcLiaison.addLogMaybe(c, ref r);
        return r;
    }
}

class RestMgr: I07RESTServable {// 🦜 : RESTful service manager
    public required I05ForRESTPoolAccessible i5 {get; init;} // pool manager
    public required I06ForRESTChainInfoGettable i6 {get; init;} // queue manager
    public required I08ForRESTChainGettable i8 {get; init;} // the archive manager

    public W<PG2<TChain.Info>> R02_getRChainInfos0(bool todo, string u = "", int q01_pageIdx = 1,  int q03_pageSize = 10) {

        // 🦜 : If todo: get from pool(i5), else, get from both (i5,i6)
        IList<TChain.Info> l = this.i5.F02_getChInfos(u);
        if (!todo){
            IList<TChain.Info> l2 = this.i6.F02_getChInfos(u);
            foreach (var i in l2) l.Add(i);
            IList<TChain.Info> l3 = this.i8.F02_getChInfos(u);
            foreach (var i in l3) l.Add(i);
        }
        return N.OK(RestMgr.getPagedResult(l, q01_pageIdx, q03_pageSize));
    }

    public W<string> R03_pauseChain(string cid){
        this.i5.F04_notifyPauseChain(cid);
        return N.OK();
    }

    public W<string> R04_resumeChain(string cid){
        this.i5.F05_notifyResumeChain(cid);
        return N.OK();
    }

    public async Task<W<string>> R05_getChainById0(string cid, string u = "") {
        TChain? c = this.i5.F03_getChain(cid, u);
        if (c == null){
            c = await this.i8.F03_getChainAsync(cid, u);
        }
        return N.OK(N.toJson(c ?? throw new InvalidOperationException($"Chain with cid {cid} not found.")));
    }

    public W<string> R06_postChain0(string ch) {
        this.i5.F01_updateCh(TChain.FromJson(ch));
        return N.OK();
    }


    public static PG2<T> getPagedResult<T>(IList<T> l, int q01_pageIdx,  int q03_pageSize) {
        // 🦜 : use the Linq magic .Skip
        var l0 = l.Skip((q01_pageIdx - 1) * q03_pageSize).Take(q03_pageSize).ToList();
        PG2<T> r = new(l0, l.Count, (int)Math.Ceiling((double)l.Count / q03_pageSize));
        return r;
    }
}

class JsonlArchiver : I04Archivable,
    I08ForRESTChainGettable { // 🐝
    public string p1 {get; init;} // path to the archive folder
    public string p0 => Path.Combine(this.p1, "hi.jsonl"); // path to the archive file
    IDictionary<string, TChain.Info> M = new Dictionary<string, TChain.Info>(); // cid, info

    public JsonlArchiver(string p1){
        this.p1 = p1;
        if (!Directory.Exists(p1))
            throw new DirectoryNotFoundException($"Base folder does not exist: {p1}");
    }

    public IList<TChain.Info> F02_getChInfos(string u){
        IList<TChain.Info> L = new List<TChain.Info>();
        lock (this.M){
            foreach (var kvp in this.M) {
                TChain.Info x = kvp.Value;
                if (N.checkUserToken(u, x.creator)) { // check user token
                    L.Add(x);
                }
            }
        }
        return L;
    }

    public async Task Arch(TChain x) {
        if (string.IsNullOrEmpty(x.cid)) {
            throw new InvalidOperationException("Chain ID (cid) must be set before archiving.");
        }
        // 1. for each ChainItem
        foreach (ChainItem c in x.L){
            // 2. set cid
            c.cid = x.cid;
            // 3. dump it to JSON
            JsonObject o = await c.dumpAndGetArchiveJson(baseFolder:this.p1);
            // 🐝 : okay... it seems like we can't await in lock?... so maybe we will just hope that it works..
            // append line to p0
            lock(this.p0){      // 🦜 : <2025-08-08 Fri>: since we are trying to retrieve chain. now lock it
                File.AppendAllText(this.p0, o.ToJsonString() + "\n");
            }
        }
        // 2. save a copy of Info inside M:
        // 2.1 check the logs of last item
        string? err = JsonlArchiver.getErrorMaybe(x.L.Last().logs);
        if (err != null) {
            S.LogInfo($"🐝 Archiving chain {x.cid} with error: {err}");
        } else {
            S.LogInfo($"🐝 Archiving chain {x.cid} with no error.");
        }
        lock (this.M){
            M[x.cid] = x.GetInfo(status: err ?? $"Done at {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        }
    }

    static string? getErrorMaybe(string s){ // get from logs
        string p = @"<error time=(.*)>([\s\S]*)</error>"; // \s\S = whitespace + nonwhitespace = everything
        Match m = Regex.Match(s, p);
        if (m.Success) {
            return "Error at:" + m.Groups[1].Value + ": " + m.Groups[2].Value; // get the status from the log
        }
        return null;
    }

    public async Task<TChain?> F03_getChainAsync(string cid, string u){
        // this is for the chain-digging archiver..
        // 1. try f
        IList<JsonObject> L = this.readAllJson();
        if (L.Count == 0) return null;
        // 2. 
        IEnumerable<IGrouping<string, JsonObject>> G = L.GroupBy(o => N.GetString(o,"cid"));

        foreach (IGrouping<string,JsonObject> g in G){
            if (g.Key != cid) continue;
            // else try parse the chain
            (TChain c, string status) = await JsonlArchiver.tryChainFromGroup(g, this.p1);
            if (N.checkUserToken(u, c.creator)){
                return c;
            }
        }
        return null;
    }

        IList<JsonObject> readAllJson(){
            IList<JsonObject> L = new List<JsonObject>();
            lock (this.p0){
                if (!File.Exists(this.p0)) return new List<JsonObject>(); // no file, no chains
                using (StreamReader r = new StreamReader(this.p0)) {
                    string? ln;
                    while ((ln = r.ReadLine()) != null) {
                        JsonObject o = JsonNode.Parse(ln) as JsonObject ?? throw new JsonException($"🐝 Invalid JSON format while studying {p0}");
                        L.Add(o);
                    }
                }
            }
            return L;
        }

    public async Task<int> study(){         // 🦜 : study hi.jsonl, return the next available cid
        // 1. read all elements
        IList<JsonObject> L = this.readAllJson();
        if (L.Count == 0) return 0;
        // 2. group by cid
        IEnumerable<IGrouping<string, JsonObject>> gs = L.GroupBy(o => N.GetString(o,"cid"));
        // 3. figure out the max cid-N
        int I  = 0;
        string p = "cid-([0-9]+)";
        foreach (IGrouping<string,JsonObject> g in gs) {
            // 4. parse the cid-N
            Match m = Regex.Match(g.Key, p);
            if (m.Success && int.TryParse(m.Groups[1].Value, out int n)) {
                I = Math.Max(I, n); // get the max
            }
            // 5. try figure out a TChain.Info from the group
            this.M[g.Key] = await tryInfoFromGroup(g);
        }

        return I + 1; // return the next available cid

        async Task<TChain.Info> tryInfoFromGroup(IGrouping<string, JsonObject> g){
            (TChain c, string status) = await JsonlArchiver.tryChainFromGroup(g, this.p1);
            return c.GetInfo(status:status);

        }
    }

    static async Task<(TChain, string)> tryChainFromGroup(IGrouping<string, JsonObject> g, string p1){
        //  🦜: updated version, try start a new chain
        // 1. start a new chain
        var c = new TChain {cid = g.Key};
        // 2. for each item in the group
        string status = $"done at {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; // default status
        foreach (JsonObject o in g) {
            // 2.1 update the payload json
            o["P"] = await tryRecoverPayload(N.GetString(o, "P"), p1);
            // 2.2 load the ChainItem
            ChainItem i = ChainItem.S03_fromJson(o);
            c.L.Add(i);
            // 2.3 update status is needed
            string? err = JsonlArchiver.getErrorMaybe(i.logs);
            if (err != null) status = err; // set the status
        }
        return (c, status);
        async Task<JsonObject> tryRecoverPayload(string p, string p1){
            // 1. load json: should be the payload
            JsonObject o = JsonNode.Parse(p) as JsonObject ?? throw new JsonException($"🐝 Invalid JSON format in payload: {p}");
            JsonElement v = JsonSerializer.Deserialize<JsonElement>(p);
            // 🦜 : We can't update the object on the fly, so we need two copies.. one for reading and one fore modifying

            // 2. for each k, try recover the value
            foreach (var kv in v.EnumerateObject()) {
                string k = kv.Name;
                if (!o.ContainsKey(k)) throw new InvalidOperationException($"Property `{k}` not found in {o.GetType().Name} object: {o.ToString()}");
                o[k] = await ChainItem.S02_RetrievePayloadAccordingToUrl(k:k, v: N.GetString(o,k), p1: p1);
            }
            return o;
        }
    }
}

class ToDoPoolMgr: I02Poolable, I05ForRESTPoolAccessible { // 🦋
    IDictionary<string, TChain> P = new Dictionary<string, TChain>();
    I03Queueable? i3; // queue manager
    public CancellationToken c0 {get;init;} // 🦜 = is_running

    public async Task Start(I03Queueable i3){
        S.LogDebug($"🦋 ToDoPoolMgr started");
        this.i3 = i3; // set the queue manager
        while (!this.c0.IsCancellationRequested) {
            await Task.Delay(15000, this.c0); // wait for 15 seconds before next iteration
            lock(this.P){
                check_and_count_em_down(P); // 🦜 : can't await inside a lock
            }
        }

        void check_and_count_em_down(IDictionary<string, TChain> P) { // 🦜: inside this, we already have the lock
            // 🦜 : process the pool: CountDown every chain if it's not paused
            foreach (var kvp in P) {
                TChain x = kvp.Value;
                if (x.paused) continue; // skip paused chains
                S.LogDebug($"🦋 Counting down chain {S.GREEN}{x.cid}{S.NOR}, length = {S.GREEN}{x.L.Count}{S.NOR}");
                bool done = x.CountDown(); // decrement the counter
                if (done) auto_green_this_and_put_2_queue(x);
            }
            void auto_green_this_and_put_2_queue(TChain x) {
                foreach (ChainItem i in x.L) {
                    if (i.grey) continue; // skip greyed items
                    if (!i.green) {
                        i.setGreen(); // set the item as green
                      }
                }
                this.P.Remove(x.cid);
                this.i3.Enq(x); // enqueue the chain for further processing
            }
        }
    }

    public void Add(TChain x) {
        S.LogDebug($"🦋 Add: adding chain with cid={x.cid}");
        x.ResetCounter();
        // 🦜 : if the last one of x is of type N.TestResult, pause it. <2025-08-11 Mon> : 🦜 : not needed anymore <2025-08-18 Mon>: 🦜 : needed again...
        if (x.L[^1].klass == N.TestResult) x.Pause();
        lock (this.P) {
            if (this.P.ContainsKey(x.cid)) {
                throw new InvalidOperationException($"Chain with cid {x.cid} already exists.");
            }
            this.P[x.cid] = x; // add to the pool
            S.LogDebug($"🦋 Add: chain {x.cid} added to pool, total chains: {this.P.Count}");
        }
    }

    public void F01_updateCh(TChain x) {
        string cid = x.cid;
        // 0. if the cid is empty, this is a new chain, we set the cid
        if (x.cid.Trim().Length == 0){
            x.cid = TChain.GetNextCid();
            S.LogDebug($"🦋 F01_updateCh: chain has no cid, generating a new one 🌱: {S.CYAN}{x.cid}{S.NOR}");
                                     }else{
            // 1. remove cid in the Map (must exist)
            lock(this.P) {
        S.LogDebug($"🦋 F01_updateCh: updating chain with cid={S.CYAN}{cid}{S.NOR}");
                if (!this.P.ContainsKey(cid)) {
                    S.LogDebug("🦋 Current keys are:");
                    foreach (var key in this.P.Keys) {
                        S.LogDebug($"  - {key}");
                    }
                    throw new KeyNotFoundException($"Chain with cid {S.CYAN}{cid}{S.NOR} not found.");
                }
                this.P.Remove(cid);
                S.LogDebug($"🦋 F01_updateCh: removed chain {S.CYAN}{cid}{S.NOR} from pool, remaining chains: {S.CYAN}{this.P.Count}{S.NOR}");
            }
        }
        // 2. put the chain to Q
        this.i3.Enq(x); // enqueue the chain for further processing
        S.LogDebug($"🦋 F01_updateCh: chain {S.CYAN}{cid}{S.NOR} enqueued for processing");
    }

    public IList<TChain.Info> F02_getChInfos(string u){
        S.LogDebug($"🦋 F02_getChInfos: getting chain infos for user={u}");
        List<TChain.Info> l = new List<TChain.Info>();
        lock (this.P) {
            foreach (var kvp in this.P) {
                if (N.checkUserToken(u:u, creator:kvp.Value.creator)) {
                    l.Add(kvp.Value.GetInfo(status: N.Todo));
                }
            }
            S.LogDebug($"🦋 F02_getChInfos: returning {l.Count} chains for user {u}, total pool size: {this.P.Count}");
        }
        return l;
    }

    public TChain? F03_getChain(string cid, string u) {
        S.LogDebug($"🦋 F03_getTodoChain: getting chain cid={cid} for user={u}");
        TChain? ch = null;
        lock (this.P) {
            if (this.P.TryGetValue(cid, out ch)) {
                // found ✅️
                if (ch.ed != "") {
                    S.LogDebug($"🦋 ❌️ F03_getTodoChain: chain {cid} is occupied by {ch.ed}");
                    return null; // being occupied by others
                }
                ch.Pause(); ch.ed = u; // pause and occupy
                S.LogDebug($"🦋 F03_getTodoChain: chain {cid} paused and assigned to user {u}");
                return ch; // return the chain if found
            }
            S.LogDebug($"🦋 F03_getTodoChain: chain {cid} not found in pool");
            return null; // not found
        }
    }

    public void F04_notifyPauseChain(string cid){
        S.LogDebug($"🦋 F04_notifyPauseChain: pausing chain cid={cid}");
        lock (this.P) {
            if (!this.P.ContainsKey(cid)) {
                S.LogDebug($"🦋 F04_notifyPauseChain: chain {cid} not found in pool");
                return;
            }
            this.P[cid].Pause();
            S.LogDebug($"🦋 F04_notifyPauseChain: chain {cid} paused successfully");
        }
    }

    public void F05_notifyResumeChain(string cid){
        S.LogDebug($"🦋 F05_notifyResumeChain: resuming chain cid={cid}");
        lock (this.P) {
            if (!this.P.ContainsKey(cid)) {
                S.LogDebug($"🦋 F05_notifyResumeChain: chain {cid} not found in pool");
                return;
            }
            this.P[cid].Resume();
            this.P[cid].ed = "";
            S.LogDebug($"🦋 F05_notifyResumeChain: chain {cid} resumed and editor cleared");
        }
    }
}

class ToExeQueueMgr : I03Queueable, I06ForRESTChainInfoGettable{
    Queue<TChain> q = new Queue<TChain>();

    public CancellationToken c0 {get;init;} // 🦜 = is_running
    public required I01Backable i1 {get; init;}
    public required I02Poolable i2 {get; init;}
    public required I04Archivable i4 {get; init;}

    IDictionary<string, TChain.Info> ing = new Dictionary<string, TChain.Info>();
    // 🦜 : the processing chain that's being processing (for showing)

    public IList<TChain.Info> F02_getChInfos(string u){
        S.LogDebug($"🐦 F02_getChInfos: user={u}, queue size={this.q.Count}");
        List<TChain.Info> l = new List<TChain.Info>();
        lock (this.q) {
            int n1 = this.q.Count;
            foreach (var (i,x) in this.q.Select((x, i) => (i, x))) {
                if (N.checkUserToken(u:u, creator:x.creator)) {
                    l.Add(x.GetInfo(status: N.Queue + $" {i+1}/{n1}")); // add index and total count
                }
            }
        }

        // <2025-08-05 Tue> also add the ing chains
        lock (this.ing) {
            foreach (var kvp in this.ing) {
                var x = kvp.Value;
                if (N.checkUserToken(u:u, creator:x.creator)) {
                    l.Add(x);
                }
            }
        }
        S.LogDebug($"🐦 F02_getChInfos: returning {l.Count} chains");
        return l;
    }

    public async Task Start(int Nt){// number of parallel tasks
        S.LogInfo($"🦜 : Starting the queue manager, waiting for items to process...");
        Task[] Ts = new Task[Nt]; // Move task array outside the loop
        
        while (!this.c0.IsCancellationRequested) {
            TChain? x = null;

            // 1. if there're vacancy in Ts: just add it
            int i0 = tryFindSlot();
            if (i0 == -1) {
                S.LogDebug($"🐦: All slots busy, waiting for a task to complete...");
                await Task.WhenAny(Ts);
                i0 = tryFindSlot();
                if (i0 == -1){
                    throw new InvalidOperationException("❌️️Fatal...unexpected Task.WhenAny.");
                }
            }

            lock(this.q){
                if (this.q.Count > 0) {
                    S.LogDebug($"🐦: Queue count = {S.GREEN} {this.q.Count}{S.NOR} , popping item");
                    x = this.q.Dequeue();
                }
            }
            if (x != null) {
                // 2. else: wait for the first one to finish
                // Find an available slot in the task array
                Ts[i0] = DoItGentlely(x);
                S.LogDebug($"🐦: Started processing chain {S.GREEN}{x.cid}{S.NOR} in slot {i0}");
            }

            await Task.Delay(1000, this.c0); // wait for 1 second before next iteration

            int tryFindSlot(){
                for (int i = 0; i < Ts.Length; i++) {
                    if (Ts[i] == null || Ts[i].IsCompleted) {
                        return i; // return the first available slot
                    }
                }
                return -1;
            }
            async Task DoItGentlely(TChain x){
                try {
                    // add the chainInfo to the ing dictionary
                    lock (this.ing) {
                        this.ing[x.cid] = x.GetInfo(status: $"Processing started at {DateTime.Now:HH:mm:ss}");
                    }
                    // 
                    await this.process(x);
                } catch (Exception e) {
                    // log the error and continue
                    x.L.Last().LogError(e);
                    S.LogInfo($" Error processing chain {S.GREEN} {x.cid}: {S.GREEN} {e.Message}, dumping it");
                    await this.i4.Arch(x); // 🦜 : log and save 
                }finally {
                    // 🦜 : remove from ing if exists
                    lock (this.ing) {
                        this.ing.Remove(x.cid);
                        S.LogDebug($"🐦: Removed chain {S.GREEN}{x.cid}{S.NOR} from processing queue");
                    }
                }
            }
        }
    }
    public void Enq(TChain item) {
        S.LogDebug($"🐦 : Enqueuing item with cid: {S.GREEN}{item.cid}{S.NOR}");
        lock(this.q) {
            this.q.Enqueue(item);
        }
    }

    async Task process(TChain x) {
        S.LogDebug($"🐦 :processing chain {S.GREEN}{x.cid}{S.NOR}, length = {S.GREEN}{x.L.Count}{S.NOR}");
        (int i, ChainItem? o) = x.findLastNonGreyItem(); // C# 12
        if (o == null){
            S.LogInfo($" Whole chained greyed out: done");
            await this.i4.Arch(x);
            return;
        }else if (o.green){
            S.LogDebug($"🐦 process: found green item of color {S.GREEN}{o.color}{S.NOR}");
            await process_green(x, i, o);
        }else{
            S.LogDebug($"🐦 process: found non-green item, adding to pool");
            // process_non_green(x, i, o);
            this.i2.Add(x); // -> human-todo
        }

        async Task process_green(TChain x, int i, ChainItem o){
            S.LogDebug($"🐦 process_green: processing color {S.GREEN}{o.color}{S.NOR}");
            if(o.color == N.Maroon){
                S.LogDebug($"🐦 process_green: handling Maroon (InitialInput)");
                await p00_maroon(); return;
            }else if (o.color == N.Red){
                S.LogDebug($"🐦 process_green: handling Red (TextSoW)");
                await p01_red(); return;
            }else if (o.color == N.Blue){
                S.LogDebug($"🐦 process_green: handling Blue (TestScript)");
                await p04_blue(); return;
            }else if (o.color == N.Purple){
                S.LogDebug($"🐦 process_green: handling Purple (TestResult) - archiving");
                await p05_purple(); return;
            }

            var g = new HashSet<string>([N.Yellow, N.Orange]);
            if (g.Contains(o.color)){
                S.LogDebug($"🐦 process_green: handling Yellow/Orange pair processing");
                await p02_yellowOrangeFriends(); return;
            }

            async Task p00_maroon(){
                S.LogDebug($"🐦 p00_maroon: parsing InitialInput");
                var o1 = (C00_InitialInput) o;
                var y = await this.i1.F0_ParseInitialInput(o1);
                // append the new item and queue it for further processing
                x.L.Add(y);
                S.LogDebug($"🐦 p00_maroon: added TextSoW, moving to pool");
                this.i2.Add(x); // -> human-todo
                // this.Enq(x);        // 🦜 : we can also re queue it, but now let's just pool it.
            }

            async Task p01_red(){
                S.LogDebug($"🐦 p01_red: processing TextSoW to generate test cases");
                var o1 = (C01_TextSoW) o; // convert to subclass
                IList<C03_TestCase> l = await this.i1.F1_TextSoW2Testcases(o1);
                C02_NetworkSetup e = await this.i1.F2_GetCommonNetworkSetup(o1);
                S.LogDebug($"🐦 p01_red: generated {l.Count} test cases");
                // append them all
                foreach (C03_TestCase c in l){
                    var x1 = (TChain) x.Clone();
                    x1.L.Add(e); x1.L.Add(c);
                    this.i2.Add(x1); // -> human-todo
                }
            }

            async Task p02_yellowOrangeFriends(){
                var ks = new HashSet<string>([N.TestCase, N.NetworkSetup]); // 🦜 : the keys we are looking for
                IDictionary<string, ChainItem> s = x.findFriends(ks); // klazz name -> ChainItem
                /*
                  if both found: -> F3
                  if NetworkSetup not found -> try find TextSoW -> if found -> call F2 -> todo;
                                                                -> else: throw
                  if TestCases not found -> try find TextSoW -> if found -> F1 -> todo;
                                                                -> else: throw
                  if non-found: thow data corruption ❌️
                */
                if (s.Count == ks.Count) {
                    S.LogDebug($"🐦 p02_yellowOrangeFriends: both NetworkSetup and TestCases found, generating script");
                    C02_NetworkSetup n = (C02_NetworkSetup ) s[N.NetworkSetup];
                    C03_TestCase c = (C03_TestCase) s[N.TestCase];
                    // call api and add
                    C04_TestScript s1 = await this.i1.F3_GenerateScript(c, n);
                    x.L.Add(s1);
                    this.i2.Add(x); // -> human-todo
                }else if (s.ContainsKey(N.NetworkSetup)) {
                    S.LogDebug($"🐦 p02_yellowOrangeFriends: NetworkSetup found,but not generating TestCases");
                    C01_TextSoW o1 = x.findLastItemOf(N.TextSoW) as C01_TextSoW ?? throw new InvalidOperationException("TextSoW not found before NetworkSetup.");
                    IList<C03_TestCase> l = await this.i1.F1_TextSoW2Testcases(o1);
                    foreach (C03_TestCase c in l){
                        var x1 = (TChain) x.Clone();
                        x1.L.Add(c);
                        this.i2.Add(x1); // -> human-todo
                    }
                }else if (s.ContainsKey(N.TestCase)) {
                    S.LogDebug($"🐦 p02_yellowOrangeFriends: TestCases found but not NetworkSetup, trying to find TextSoW");
                    C01_TextSoW o1 = x.findLastItemOf(N.TextSoW) as C01_TextSoW ?? throw new InvalidOperationException("TextSoW not found before TestCases.");
                    C02_NetworkSetup n = await this.i1.F2_GetCommonNetworkSetup(o1);
                    x.L.Add(n); // add the NetworkSetup
                    this.i2.Add(x); // -> human-todo
                }else{
                    // else
                    S.LogDebug($"🐦 p02_yellowOrangeFriends: neither NetworkSetup nor TestCases found, throwing data corruption error");
                    throw new InvalidOperationException("Data corruption: neither NetworkSetup nor TestCases found in the chain.");
                }
            }

            async Task p04_blue(){
                S.LogDebug($"🐦 p04_blue: running TestScript and getting results");
                var o1 = (C04_TestScript) o; // convert to subclass
                C02_NetworkSetup n = tryFindLastNetworkSetup();
                S.LogDebug($"🐦 p04_blue: found NetworkSetup with id={S.CYAN}{n.id}{S.NOR}");
                C05_TestResult e = await this.i1.F4_RunAndGetLog(o1, n);
                x.L.Add(e);
                S.LogDebug($"🐦 p04_blue: added TestResult, moving to pool");
                this.i2.Add(x); // -> human-todo

                C02_NetworkSetup tryFindLastNetworkSetup(){ // get the last valid network setup, throw if not found
                    var o1 = x.findLastItemOf(N.NetworkSetup);
                    if (o1 == null) throw new InvalidOperationException("Trying to run script but No valid NetworkSetup found before it.");
                    return (C02_NetworkSetup) o1;
                }

            }
            async Task p05_purple(){
                S.LogDebug($"🐦 p05_purple: TestResult completed, archiving chain");
                // It's time to archive the chain
                await this.i4.Arch(x);
            }
        }

    }
}


#endregion// --------------------------------------------------

abstract class ChainItem: ICloneable {
    public string notes ="", logs="", h="";
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public string cid=""; // 🦜 : cid is only-set during archiving
    public string klass => GetKlass();
    public IDictionary<string, string> P => GetPayloads();

    [JsonIgnore]
    public bool grey => h.Contains("greyed");
    [JsonIgnore]
    public bool green => h.Contains("greened");
    [JsonIgnore]
    public string color => this.klass;
    [JsonIgnore]
    public IList<string> hList => this.h.Split(';', StringSplitOptions.RemoveEmptyEntries);
    public string creator {
        get {                   // 🦜 : get the first "created <T> <name>"
            string s = "<anonymous-author>";
            // S.LogInfo($" parsing 🦜H list: {string.Join(", ", this.hList)}");
            foreach (var x in this.hList)
            {
                (string flag, DateTime dt, string? utk) = ChainItem.ParseLog(x);
                if (flag == "created" && utk != null)
                {
                    s = utk;
                    break;
                }
            }
            return s;
        }
    }

    public string createdAt {
        get {                   // 🦜 : get the first "created <T> <name>"
            return ChainItem.tryFindCreatedAt(this.hList);
        }
    }

    public static string tryFindCreatedAt(IList<string> hList) {
        string s = "";
        foreach (var x in hList) {
            (string flag, DateTime dt, string? utk) = ChainItem.ParseLog(x);
            if (flag == "created" && utk != null) {
                s = dt.ToString("o"); // ISO 8601 format
                break;
            }
        }
        return s;
    }

    static string getSaveFolder(string baseFolder) => Path.Combine(baseFolder, "zero");

    public async Task<JsonObject> dumpAndGetArchiveJson(
        string baseFolder = "/home/me/repo/work/n3c/bk/bin/Debug"){
        string p1 = getSaveFolder(baseFolder);
        if (!Directory.Exists(baseFolder))
            throw new DirectoryNotFoundException($"Base folder does not exist: {baseFolder}");

        // 0. start creating Json
        var o = new JsonObject { ["notes"] = this.notes, ["logs"] = this.logs,
                                 ["h"] = this.h, ["klass"] = this.klass,
                                 ["cid"] = this.cid, ["creator"] = this.creator};

        // 1. create the zero folder if needed
        if (!Directory.Exists(p1)) {
            S.LogInfo($"🌱 Creating db directory: {p1}");
            Directory.CreateDirectory(p1); // create the zero folder
        }
        // 2. dump the payload and get the payload json
        JsonObject p = await dumpAndGetPayloadsJson();
        o["P"] = JsonSerializer.Serialize(p);
        // 3. bye
        return o;

        async Task<JsonObject> dumpAndGetPayloadsJson(){
            // 1. for each k,v in payloads
            JsonObject p = new JsonObject();
            foreach (var kvp in this.GetPayloads()) {
                // 2. if v is byte[] then convert to base64
                string k = kvp.Key, v = kvp.Value;
                p[k] = await ChainItem.S01_DumpPayloadAndGetUrl(k, v, p1);
            }
            return p;
        }
    }

    record I(string k, bool isBinary, string suffix);
    static readonly I[] I0 = [
        new("sowDocx64", true, ".docx"), new("cliDocx64", true, ".docx"),
        new("pyscript", false, ".py"), new("conftestPy", false, ".py")
    ];

    public static async Task<string> S02_RetrievePayloadAccordingToUrl(string k, string v, string p1){ // reverse of S01
        // 1. figure out the path 🦜 : if v doesn't exits, try p1 / v.name and p1/zero/p1
        string p = v;
        if (!Path.Exists(p)) p = Path.Combine(p1, Path.GetFileName(v));
        if (!Path.Exists(p)) p = Path.Combine(ChainItem.getSaveFolder(p1) ,Path.GetFileName(v));
        if (!Path.Exists(p))
            throw new FileNotFoundException($"File not found for {S.CYAN}{k}{S.NOR} : v={S.CYAN}{v}{S.NOR}, p1={S.CYAN}{p1}{S.NOR}");

        // 2. read the bytes contents
        byte[] b = await File.ReadAllBytesAsync(v);

        // 3. if k is binary, convert to base64, else to UTF8 string
        (bool isBinary, string suffix) = getIsBineryAndSuffix(k);
        if (suffix != Path.GetExtension(v)) {
            throw new InvalidOperationException($"Malformed payload kv: File extension mismatch for {k}: expected {suffix}, got {Path.GetExtension(v)}");
        }
        return isBinary ? Convert.ToBase64String(b) : Encoding.UTF8.GetString(b); // return UTF8 string
    }

    public static async Task<string> S01_DumpPayloadAndGetUrl(string k, string v, string p1) {
        // ...[todo]
        (bool isBinary, string suffix) = getIsBineryAndSuffix(k);
        // 1. get byte array from v
        byte[] b;
        if (isBinary) {
            b = Convert.FromBase64String(v);
        }else{
            b = Encoding.UTF8.GetBytes(v);
        }
        // 2. hash it
        string h = f(b);
        // 3. get the path = p1 / <hash> + suffix
        string p2 = Path.Combine(p1, h + suffix);
        // 3. if path does not exist, write the file
        if (!File.Exists(p2)) {
            await File.WriteAllBytesAsync(p2, b);
        }
        // 4. return the url
        return p2;
        string f(byte[] b){     // get a valid filename from the byte content
            return Convert.ToBase64String(SHA256.HashData(b))
                .Replace('+', '-')
                .Replace('/', '_')
                .Replace("=", "");
        }
    }

    public static ChainItem S03_fromJson(JsonObject e){ // 🦜 : used in TChain.FromJson
        return ChainItem.S03_fromJson(JsonSerializer.Deserialize<JsonElement>(e));
    }
    public static ChainItem S03_fromJson(JsonElement e){ // 🦜 : used in TChain.FromJson
        var k = e.GetProperty("klass").GetString();
        var p = e.GetProperty("P");
        ChainItem i = k switch {
            N.InitialInput => C00_InitialInput.FromPayloads(p),
            N.TextSoW => C01_TextSoW.FromPayloads(p),
            N.TestCase => C03_TestCase.FromPayloads(p),
            N.NetworkSetup => C02_NetworkSetup.FromPayloads(p),
            N.TestScript => C04_TestScript.FromPayloads(p),
            N.TestResult => C05_TestResult.FromPayloads(p),
            _ => throw new InvalidOperationException($"Unknown class: {k}")
        };

        i.notes = N.GetString(e, "notes");
        i.logs = N.GetString(e, "logs");
        i.h = N.GetString(e, "h");
        return i;
    }

    public static (bool, string) getIsBineryAndSuffix(string k) {
        foreach (var x in ChainItem.I0) {
            if (x.k == k) {
                return (x.isBinary, x.suffix);
            }
        }
        return (false, ".txt"); // default to text file
    }

    public void LogInfo(string s){
        this.logs += "<info time=" + DateTime.Now.ToString("o") + ">" + s + "</info>";
    }

    public void LogError(string s){
        this.logs += "<error time=" + DateTime.Now.ToString("o") + ">" + s + "</error>";
    }

    public void LogError(Exception e){
        this.LogError(e.Message + "\n\n" + e.StackTrace);
    }

    public static (string, DateTime, string?) ParseLog(string s) {
        // parse a log entry like "flag 2025-07-21T16:38:27.1803288+08:00 user-token"
        var parts = s.Split(' ', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length < 2) throw new FormatException("Invalid log format.");
        string flag = parts[0];
        DateTime dt = DateTime.Parse(parts[1]);
        string? userToken = parts.Length > 2 ? parts[2] : null;
        return (flag, dt, userToken);
    }

    public virtual object Clone()  => this.MemberwiseClone();

    // to be implemented by subclasses
    public abstract string GetKlass();
    public abstract IDictionary<string, string> GetPayloads();

    ChainItem setF(string s, string u=""){ // flag, user-token
        this.h += $";{s} " + DateTime.Now.ToString("o");
        if (!string.IsNullOrEmpty(u)) {
            // assert u doesn't contain spaces and semicolons
            if (u.Contains(" ") || u.Contains(";")) 
                throw new FormatException("User token must not contain spaces or semicolons.");
            this.h += $" {u}";
        }
        return this;
    }

    public ChainItem setGreen() => this.setF("auto_greened");
    public ChainItem setGenerated() => this.setF("generated");

#if MY_TEST
    public ChainItem setGrey() => this.setF("greyed"); // for test only
#endif
}

class TChain : ICloneable {
    public int ct;              // counter
    public string ed = "";             // editor, = "" | "<user-token>"
    public required string cid; // chain id
    public List<ChainItem> L = new List<ChainItem>();


    public bool paused => this.ct <= 0; // is the chain paused?
    public void Pause(){
        if (!this.paused) this.ct *= -1; // pause the chain
    }
    public bool CountDown(){
        // decrement the counter, return true if zero is reached, throw if paused
        if (this.paused) throw new InvalidOperationException("Cannot count down a paused chain.");
        this.ct--;
        return this.ct <= 0;
    }
    public void Resume(){
        if (this.paused) this.ct *= -1; // resume the chain   
    }
    public void ResetCounter(int c = 30){
        if (c <= 0) throw new ArgumentOutOfRangeException(nameof(c), $"Counter must be positive. Got: {c}");
        this.ct = c;
    }

    public string creator {    // author Token = L[0].creator
        get {
            if (this.L.Count > 0) {
                return this.L[0].creator;
            }
            return "<no-creator-for-empty-chain>";
        }
    }
    public string createdAt {
        get {
            if (this.L.Count > 0) {
                return this.L.First().createdAt;
            }
            return "<no-createdAt-for-empty-chain>";
        }
    }

    public string finishedAt {
        get {
            if (this.L.Count > 0) {
                return this.L.Last().createdAt;
            }
            return "<no-createdAt-for-empty-chain>";
        }
    }

    public string projectId {
        get {
            if (this.L.Count > 0 && this.L[0] is C00_InitialInput i) {
                return i.projectId;
            }
            return "<no id: initialInput-not-found>";
        }
    }
    public string projectName {
        get {
            if (this.L.Count > 0 && this.L[0] is C00_InitialInput i) {
                return i.projectName;
            }
            return "<no name: initialInput-not-found>";
        }
    }

    public static int Cid = 0;
    public static string GetNextCid() {
        int nextCid = Interlocked.Increment(ref TChain.Cid);
        return  $"cid-{nextCid}";
    }

    public static TChain FromJson(string j) {
        var d = JsonSerializer.Deserialize<JsonDocument>(j);
        var r = d.RootElement;
        return TChain.FromJson(r);
    }
    public static TChain FromJson(JsonElement r) {
        /* on bad json, throw:
           1. JsonException :: if the JSON is invalid
           2. KeyNotFoundException :: if GetProperty fails
           3. InvalidOperationException :: if the class type is unknown
           4. FormatException :: if Convert.FromBase64String fails
        */
        var c = new TChain {cid = N.GetString(r, "cid")};
        // set chain-wise properties here...
        var l = r.GetProperty("L");

        foreach (var e in l.EnumerateArray()) {
            ChainItem i = ChainItem.S03_fromJson(e);
            // S.LogInfo($"🦜 Got h = {i.h} for klass {k}");
            c.L.Add(i);
        }
        return c;
    }


    public (int, ChainItem?) findLastNonGreyItem(int i = -1){
        // if i == 0: return (0, null);
        // if i == 4: check for 0,1,2,3. We have the result L[i] == o
        if (i < 0) i = this.L.Count;
        ChainItem? o = null;
        while (i > 0) {
            i--;
            ChainItem o1 = this.L[i];
            if (!o1.grey){
                o = o1;
                break;
            }
        }
        return (i, o);
    }

    public ChainItem? findLastItemOf(string klass){
        // the last Non-grey item of the given class
        return this.L.Where(x => x.klass == klass && !x.grey)
            .LastOrDefault(); // return the last item of the given class, or null if not found
    }


    public IDictionary<string,ChainItem> findFriends(ISet<string> ks){
        IDictionary<string, ChainItem> s = new Dictionary<string, ChainItem>();
        // for each klass in ks:
        foreach (string k in ks) {
            ChainItem? o = this.findLastItemOf(k);
            if (o == null) {
                S.LogDebug($"🦜 : No last item of klass {k} found in chain {this.cid}");
                continue; // no item of this klass found
            }
            s[k] = o; // add to the dictionary
        }
        return s;
    }
    public record Info(string cid, string ed, int ct,
                       [property:JsonPropertyName("N")] int N, // 🦜 : confusion: c# by-default lowercase s[0] = "n"
                       string creator, string status,
                       string lastKlass,
                       string createdAt, string finishedAt,
                       string projectId, string projectName);
    public Info GetInfo(string status = "<mock-status>") => new Info(cid:this.cid, ed:this.ed, ct:this.ct, N:this.L.Count,
                                                                     creator: this.creator, status:status,
                                                                     lastKlass: this.L.LastOrDefault()?.GetKlass() ?? "<no-last-klass>",
                                                                     createdAt: this.createdAt, finishedAt:this.finishedAt,
                                                                     projectId: this.projectId, projectName: this.projectName);

    public object Clone() {
        TChain o = (TChain) this.MemberwiseClone();
        o.cid = TChain.GetNextCid(); // set the cid
        S.LogDebug($"🦜 : Cloned new chain with cid={S.CYAN}{o.cid}{S.NOR}");
        o.L = new List<ChainItem>();
        foreach (var x in this.L) {
            o.L.Add((ChainItem) x.Clone()); // deep copy of each ChainItem
        }
        return o;
    }

    public static TChain Mock(
        string cid = "mock-cid",
        string projectId = "project-aaa",
        string projectName = "project aaa",
        string p0 = "/home/me/repo/work/n3c/bk/T.json" // the template file
    ){
        string s = File.ReadAllText(p0);
        s = s.Replace("<mocked-cid>",cid)
            .Replace("<mocked-projectId>",projectId)
            .Replace("<mocked-projectName>", projectName);
        var c = TChain.FromJson(s);
        c.ct = (new Random()).Next(0, 20); // random counter between 10 and 20
        return c;
    }
}

class C00_InitialInput : ChainItem{
    public required string projectId, projectName;
    public required byte[] sowDocx { get; init; }
    public required byte[] cliDocx { get; init; }

    public override string GetKlass() => N.InitialInput;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "projectId", this.projectId },
            { "projectName", this.projectName },
            { "sowDocx64", Convert.ToBase64String(this.sowDocx) },
            { "cliDocx64", Convert.ToBase64String(this.cliDocx) }
        };
    }

    public static  C00_InitialInput FromJson(string j) {
        // 🦜 : parse the JSON and return a C00_InitialInput object
        var d = JsonSerializer.Deserialize<JsonDocument>(j);
        var e = d.RootElement;
        var k = e.GetProperty("klass").GetString();
        if (k != N.InitialInput) {
            throw new InvalidOperationException($"Expected class {N.InitialInput}, got {k}");
        }
        var p = e.GetProperty("P");
        var i =  C00_InitialInput.FromPayloads(p);

        i.notes = N.GetString(e, "notes");
        i.logs = N.GetString(e, "logs");
        i.h = N.GetString(e, "h");
        return i;
    }

    public static C00_InitialInput FromPayloads(JsonElement p) {
        // 🦜 : create a C00_InitialInput from the payloads
        return new C00_InitialInput {
            projectId = N.GetString(p, "projectId"),
            projectName = N.GetString(p, "projectName"),
            sowDocx = Convert.FromBase64String(N.GetString(p, "sowDocx64")),
            cliDocx = Convert.FromBase64String(N.GetString(p, "cliDocx64"))
        };
    }

    public static C00_InitialInput Mock(){
        return new C00_InitialInput {
            projectId = "mock-project-id",
            projectName = "Mock Project",
            sowDocx = Encoding.UTF8.GetBytes("Mock SOW Document Content"),
            cliDocx = Encoding.UTF8.GetBytes("Mock CLI Document Content")
        };
    }
    public InitialInput toPb(){
        return new InitialInput {
            Notes =  this.notes,
            SowDocx = Google.Protobuf.ByteString.CopyFrom(this.sowDocx),
            CliDocx = Google.Protobuf.ByteString.CopyFrom(this.cliDocx)
        };
    }
}

/*
  i1.P.set("processedSow", "Example processed SOW content");
  i1.P.set("processedCli", "Example processed CLI content");
  i1.P.set("processedNotes", "Example processed notes content");
*/
class C01_TextSoW: ChainItem{
    public override string GetKlass() => N.TextSoW;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "processedSow", this.pSow },
            { "processedCli", this.pCli },
            { "processedNotes", this.pNotes },
            { "clarificationQuestions", this.cQ },
            { "usrAnswers", this.uA }
        };
    }

    public required string pSow, pCli, pNotes, cQ, uA;
    public static C01_TextSoW Mock(){
        return new C01_TextSoW {
            pSow = "Mock processed SOW content",
            pCli = "Mock processed CLI content",
            pNotes = "Mock processed notes content",
            cQ = "Mock clarification questions",
            uA = "Mock user answer"
        };
    }

    public TextSoW toPb(){
        return new TextSoW {
            ProcessedSow = this.pSow,
            ProcessedCli = this.pCli,
            ProcessedNotes = this.pNotes,
            ClarificationQuestions = this.cQ,
            UsrAnswers = this.uA
        };
    }

    public static C01_TextSoW FromPb(TextSoW pb) {
        return new C01_TextSoW {
            pSow = pb.ProcessedSow,
            pCli = pb.ProcessedCli,
            pNotes = pb.ProcessedNotes,
            cQ = pb.ClarificationQuestions,
            uA = pb.UsrAnswers
        };
    }

    public static C01_TextSoW FromPayloads(JsonElement p) {
        // 🦜 : create a C01_TextSoW from the payloads
        return new C01_TextSoW {
            pSow = N.GetString(p, "processedSow"),
            pCli = N.GetString(p, "processedCli"),
            pNotes = N.GetString(p, "processedNotes"),
            cQ = N.GetString(p, "clarificationQuestions"),
            uA = N.GetString(p, "usrAnswers")
        };
    }

}

class C03_TestCase: ChainItem{
    public override string GetKlass() => N.TestCase;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "name", this.name},
            { "test_steps", JsonSerializer.Serialize(this.steps) }
        };
    }

    public override object Clone(){
        var o = (C03_TestCase) this.MemberwiseClone();
        // o.steps = [.. this.steps]; // == new List(this.steps)
        o.steps = new List<E>(this.steps); // 🦜 : I prefer this...
        return o;
    }
    public record E(string step, string expected);
    public List<E> steps = new List<E>();
    public required string name;

    public static C03_TestCase Mock(){
        return new C03_TestCase {
            name = "Mock Test Case",
            steps = new List<E> {
                new E("Step 1", "Expected Result 1\u4f60\u597d"),
                new E("Step 2", "期待的结果 2")
            }
        };
    }
    /*
message TestCase {
  string name = 1;
  repeated StepPair steps = 2; // each step has an expected result
  message StepPair {
    string step = 1; // step description
    string expect = 2; // expected result
  }
     */

    public TestCase toPb(){
        var testCase = new TestCase {
            Name = this.name
        };
        foreach (var step in this.steps) {
            testCase.Steps.Add(new TestCase.Types.StepPair {
                Step = step.step,
                Expect = step.expected
            });
        }
        return testCase;
    }
    public static C03_TestCase FromPb(TestCase pb) {
        var testCase = new C03_TestCase {
            name = pb.Name
        };
        foreach (var stepPair in pb.Steps) {
            testCase.steps.Add(new E(stepPair.Step, stepPair.Expect));
        }
        return testCase;
    }
    /*
      message TestCases {repeated TestCase test_cases = 1;}
     */
    public static IList<C03_TestCase> FromPb(TestCases p) {
        return p.TestCases_.Select(C03_TestCase.FromPb).ToList();
    }

    // --------------------------------------------------
    public static C03_TestCase FromPayloads(JsonElement p) {
        var testCase = new C03_TestCase {
            name = N.GetString(p, "name"),
        };
        string stepsJson = N.GetString(p, "test_steps");
        List<E> stepsList = JsonSerializer.Deserialize<List<E>>(stepsJson) ?? throw N.E0("Failed to deserialize 'test_steps' to List<E>");
        testCase.steps = stepsList;
        return testCase;
    }
}

class C02_NetworkSetup: ChainItem {
    public override string GetKlass() => N.NetworkSetup;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "id", this.id },
            { "topox", this.topox },
            { "conftestPy", this.conftestPy }
        };
    }
    public required string id, topox, conftestPy;
    public static C02_NetworkSetup Mock(){
        return new C02_NetworkSetup {
            id = "mock-network-setup-id",
            topox = "Mock Topology XML Content",
            conftestPy = "Mock Conftest Python Script Content"
        };
    }
    public NetworkSetup toPb(){
        return new NetworkSetup {
            Id = this.id,
            Topox = this.topox,
            ConftestPy = this.conftestPy
        };
    }
    public static C02_NetworkSetup FromPb(NetworkSetup pb) {
        return new C02_NetworkSetup {
            id = pb.Id,
            topox = pb.Topox,
            conftestPy = pb.ConftestPy
        };
    }

    public static C02_NetworkSetup FromPayloads(JsonElement p) {
        return new C02_NetworkSetup {
            id = N.GetString(p, "id"),
            topox = N.GetString(p, "topox"),
            conftestPy = N.GetString(p, "conftestPy")
        };
    }
}

class C04_TestScript: ChainItem{
    public override string GetKlass() => N.TestScript;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "pyscript", this.pyscript }
        };
    }

    public required string pyscript;
    public static C04_TestScript Mock(){
        return new C04_TestScript {
            pyscript = "Mock Python Script Content"
        };
    }
    public static C04_TestScript FromPb(TestScript pb) {
        return new C04_TestScript {
            pyscript = pb.Pyscript
        };
    }
    public static C04_TestScript FromPayloads(JsonElement p) {
        return new C04_TestScript {
            pyscript = p.GetProperty("pyscript").GetString() ?? throw N.E1("pyscript",p)
        };
    }
}

class C05_TestResult: ChainItem{
    public override string GetKlass() => N.TestResult;
    public override IDictionary<string, string> GetPayloads() {
        return new Dictionary<string, string> {
            { "status", this.status},
            { "log", this.l },  // 🦜 : log , not logs
            { "script", this.script }
        };
    }

    public static C05_TestResult FromPayloads(JsonElement p) {
        return new C05_TestResult {
            status = N.GetString(p, "status"),
            l = N.GetString(p, "log"),
            script = N.GetString(p, "script")
        };
    }

    public required string status,l,script;
    public static C05_TestResult Mock(){
        return new C05_TestResult {
            status = "Mock Status",
            l = "Mock Log Content",
            script = "Mock script Content"
        };
    }

    public TestResult toPb(){
        return new TestResult {
            Status = this.status, Log = this.l, Script = this.script
        };
    }
    public static C05_TestResult FromPb(TestResult pb) {
        return new C05_TestResult {status = pb.Status, l = pb.Log, script = pb.Script};
    }

}


